Example PRO

import { Button, List, Navigation, NavigationStack, Path, Script, Text, useState } from "scripting"

const folder = Path.join(FileManager.temporaryDirectory, `audio-input-${Date.now()}-${Math.random().toString(16).slice(2)}`)
let created = false
let engine: AVAudioEngine | null = null
let pending: Promise<void> | null = null
let cancelled = false
let alive = true
const preview = new AVPlayer()
async function stop() { cancelled = true; await engine?.stop() }
function Demo() {
  const dismiss = Navigation.useDismiss()
  const [message, setMessage] = useState("Microphone → high-pass EQ → output. Monitoring is off by default.")
  const [busy, setBusy] = useState(false)
  const [monitor, setMonitor] = useState(false)
  const [result, setResult] = useState<string | null>(null)
  const report = (text: string) => { if (alive) setMessage(text) }
  async function run(voiceProcessing: boolean) {
    if (pending) return
    cancelled = false; setBusy(true); setResult(null); preview.pause()
    pending = (async () => {
      let dry: AVAudio.Observation | null = null, wet: AVAudio.Observation | null = null
      let eq: AVAudioUnitEQ | null = null, writer: AVAudioFile | null = null
      let unsubscribe: (() => void) | null = null
      let written = 0, gaps = 0
      try {
        if (!created) { await FileManager.createDirectory(folder, true); created = true }
        const path = Path.join(folder, `recording-${Date.now()}.caf`)
        await SharedAudioSession.configure({ category: "playAndRecord", mode: voiceProcessing ? "voiceChat" : "default", options: ["defaultToSpeaker"] })
        await SharedAudioSession.setActive(true)
        engine = await AVAudioEngine.create()
        const input = await engine.getInputNode()
        if (cancelled) return
        if (voiceProcessing) await input.configureVoiceProcessing({ enabled: true, automaticGainControl: true })
        const format = await input.getOutputFormat(0)
        eq = await AVAudioUnitEQ.create({ numberOfBands: 1 }); await engine.attach(eq)
        await eq.configureBand(0, { filterType: "highPass", frequency: 180, bypass: false })
        const mixer = await engine.getMainMixerNode(); await mixer.setOutputVolume(monitor ? 0.15 : 0)
        await engine.connect(input, eq, { format }); await engine.connect(eq, mixer, { format })
        if (voiceProcessing) await engine.connect(mixer, await engine.getOutputNode(), { format })
        dry = await input.installTap({ format, slotCount: 8, overflow: "latest" })
        const outputFormat = await eq.getOutputFormat(0)
        wet = await eq.installTap({ format: outputFormat, slotCount: 8, overflow: "dropOldest" })
        writer = await AVAudioFile.openForWriting(path, outputFormat.settings)
        unsubscribe = engine.onEvent(event => {
          if (["configurationChanged", "interruption", "mediaServicesReset"].includes(event.type)) {
            cancelled = true
            report(`${event.type}: recording stopped. After the route/permission is ready, start a fresh recording.`)
            void engine?.stop().catch(error => report(String(error)))
          }
        })
        if (cancelled) return
        await engine.start()
        while (!cancelled) {
          let dryRMS = 0
          const before = await dry.read()
          if (before) {
            try {
              const samples = await before.buffer.copyInterleaved()
              for (const sample of samples) dryRMS += sample * sample
              dryRMS = Math.sqrt(dryRMS / Math.max(1, samples.length))
            } finally { await before.buffer.dispose() }
          }
          const after = await wet.read()
          if (after) {
            try {
              await writer.write(after.buffer); written += after.buffer.frameLength
              if (after.discontinuity) gaps++
              const samples = await after.buffer.copyInterleaved()
              let sum = 0; for (const sample of samples) sum += sample * sample
              report(`${voiceProcessing ? "Voice processing" : "Microphone"}: ${format.sampleRate} Hz / ${format.channelCount} channels\nBefore EQ RMS ${dryRMS.toFixed(4)} · after EQ RMS ${Math.sqrt(sum / Math.max(1, samples.length)).toFixed(4)}\nSaved ${written} frames · discontinuities ${gaps}\nMonitoring ${monitor ? "on at 15%" : "off"}`)
            } finally { await after.buffer.dispose() }
          }
          await new Promise<void>(resolve => setTimeout(resolve, 50))
        }
        await writer.close(); writer = null
        if (written && alive) setResult(path)
        report(`Stopped. Saved ${written} frames; discontinuities ${gaps}. A discontinuity means the file has a gap. Start again to rebuild for a new route.`)
      } finally {
        unsubscribe?.(); await engine?.stop(); await dry?.close(); await wet?.close()
        await writer?.close().catch(error => {
          const failure = error as AVAudio.Error
          report(`${String(error)}${failure.partialFilePath ? `\nRecovery file: ${failure.partialFilePath}` : ""}`)
        })
        await engine?.dispose(); engine = null; await eq?.dispose()
        await SharedAudioSession.setActive(false)
      }
    })().catch(error => report(String(error))).finally(() => { pending = null; if (alive) setBusy(false) })
    await pending
  }
  async function playResult() {
    try {
      if (!result) return
      await SharedAudioSession.setCategory("playback", []); await SharedAudioSession.setActive(true)
      preview.onReadyToPlay = () => preview.play()
      if (!preview.setSource(result)) throw Error("Cannot open the recording")
    } catch (error) { report(String(error)) }
  }
  return <NavigationStack><List navigationTitle="Record and Process" toolbar={{ cancellationAction: <Button title="Close" action={dismiss} /> }}>
    <Text>{message}</Text>
    <Text>Use headphones before enabling live monitoring to avoid acoustic feedback. Recordings are temporary; copy a file elsewhere before closing to keep it.</Text>
    <Button title={monitor ? "Disable monitoring for next recording" : "Enable quiet monitoring for next recording"} action={() => setMonitor(!monitor)} disabled={busy} />
    <Button title="Record microphone with EQ" action={() => { void run(false) }} disabled={busy} />
    <Button title="Record with voice processing and EQ" action={() => { void run(true) }} disabled={busy} />
    <Button title="Stop recording" action={() => { void stop().catch(error => report(String(error))) }} />
    <Button title="Play saved recording" action={() => { void playResult() }} disabled={busy || !result} />
    <Button title="Pause preview" action={() => preview.pause()} />
    {result ? <Text>{result}</Text> : null}
  </List></NavigationStack>
}
async function main() {
  try { await Navigation.present({ element: <Demo /> }) }
  finally { try { alive = false; await stop(); await pending; preview.dispose(); await SharedAudioSession.setActive(false); if (created) await FileManager.remove(folder); } finally { Script.exit() } }
}
void main()